package info.thereisonlywe.core.essentials;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class IOEssentials {

	public static final int FILE_UPDATE_POLICY_NEVER = 0;
	public static final int FILE_UPDATE_POLICY_IF_POSSIBLE = 1;
	public static final int FILE_UPDATE_POLICY_STRICT = 2;
	public static final int FILE_UPDATE_POLICY_FORCED = 3;

	public static final int NETWORK_TIMEOUT_SHORT = 1000; // in millis
	public static final int NETWORK_TIMEOUT_LONG = 15000;

	public static class FileExtensionFilter implements FilenameFilter {

		private final String ext;

		public FileExtensionFilter(String ext)
		{
			this.ext = ("." + ext).toLowerCase();
		}

		@Override
		public boolean accept(File dir, String name)
		{
			return name.toLowerCase().endsWith(ext);
		}

	}

	// http://www.devx.com/getHelpOn/10MinuteSolution/20447
	public static void extractZip(String filename)
	{
		Enumeration entries;
		ZipFile zipFile;
		try
		{
			zipFile = new ZipFile(filename);
			entries = zipFile.entries();
			while (entries.hasMoreElements())
			{
				ZipEntry entry = (ZipEntry) entries.nextElement();
				if (entry.isDirectory())
				{
					(new File(entry.getName())).mkdir();
					continue;
				}
				IOEssentials.copy(
						zipFile.getInputStream(entry),
						new BufferedOutputStream(new FileOutputStream(entry
								.getName())));
			}
			zipFile.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
	}

	// http://stackoverflow.com/questions/813710/java-1-6-determine-symbolic-links
	public static boolean isSymlink(File file) throws IOException
	{
		if (file == null)
			throw new NullPointerException("File must not be null");
		File canon;
		if (file.getParent() == null)
		{
			canon = file;
		}
		else
		{
			File canonDir = file.getParentFile().getCanonicalFile();
			canon = new File(canonDir, file.getName());
		}
		return !canon.getCanonicalFile().equals(canon.getAbsoluteFile());
	}

	public static String[] fetchValues(File file, String prefix,
			char valueSeparator)
	{
		ArrayList<String> rawResult = new ArrayList<String>();
		try
		{
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (strLine.startsWith(prefix))
				{
					StringBuilder value = new StringBuilder();
					for (int i = prefix.length(); i < strLine.length(); i++)
					{
						char ch = strLine.charAt(i);
						if (ch == valueSeparator)
						{
							rawResult.add(value.toString());
							value = new StringBuilder();
						}

						else
						{
							value.append(ch);
						}
					}
					rawResult.add(value.toString());
				}
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
			return null;
		}
		if (rawResult.isEmpty()) return null;
		else
		{
			String[] result = new String[rawResult.size()];
			return rawResult.toArray(result);
		}
	}

	public static String fetchValue(File file, int lineNo)
	{
		try
		{
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			int counter = 1;
			while ((strLine = br.readLine()) != null)
			{
				if (counter == lineNo)
				{
					br.close();
					return strLine;
				}
				counter++;
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
			return null;
		}
		return null;
	}

	public static ArrayList<String> fetchLinesEndingWith(String s, File file)
	{
		ArrayList<String> result = new ArrayList<String>();
		try
		{
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (!strLine.endsWith(s)) continue;
				result.add(strLine);
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return result;
	}

	public static ArrayList<String> fetchLinesStartingWith(String s, File file)
	{
		ArrayList<String> result = new ArrayList<String>();
		try
		{
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (!strLine.startsWith(s)) continue;
				result.add(strLine);
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return result;
	}

	public static ArrayList<String> fetchLinesLike(String s, File file)
	{
		ArrayList<String> result = new ArrayList<String>();
		try
		{
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (!strLine.contains(s)) continue;
				result.add(strLine);
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return result;
	}

	public static String readString(File file)
	{
		StringBuilder result = new StringBuilder();
		try
		{
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			boolean first = true;
			while ((strLine = br.readLine()) != null)
			{
				if (!first) strLine = "\n" + strLine;
				else first = false;
				result.append(strLine);
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return result.toString();
	}

	public static byte[] readBytes(File file) throws IOException
	{
		byte[] bytes;
		InputStream is = new FileInputStream(file);
		long length = file.length();
		if (length > Integer.MAX_VALUE)
		{
			is.close();
			return null;
		}
		bytes = new byte[(int) length];
		int offset = 0;
		int numRead = 0;
		while (offset < bytes.length
				&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0)
		{
			offset += numRead;
		}
		if (offset < bytes.length)
		{
			SystemEssentials.Logger.log(Level.WARNING, new IOException(
					"Could not completely read file"));
		}
		is.close();
		return bytes;
	}

	public static boolean copy(File sourceFile, File destFile)
	{
		FileChannel source = null;
		FileChannel destination = null;
		try
		{
			if (!destFile.exists())
			{
				if (!destFile.createNewFile()) return false;
			}
			source = new FileInputStream(sourceFile).getChannel();
			destination = new FileOutputStream(destFile).getChannel();
			destination.transferFrom(source, 0, source.size());
			return true;
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
			return false;
		}
	}

	public static void write(String data, File file, boolean append)
	{
		if (file.getParentFile() != null) file.getParentFile().mkdirs();
		try
		{
			FileOutputStream fstream = new FileOutputStream(file, append);
			DataOutputStream out = new DataOutputStream(fstream);
			BufferedWriter FW = new BufferedWriter(new OutputStreamWriter(out,
					"UTF-8"));
			FW.write(data);
			FW.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
	}

	public static void write(char[] text, File file, boolean append)
	{
		try
		{
			file.getParentFile().mkdirs();
			FileOutputStream fstream = new FileOutputStream(file, append);
			DataOutputStream out = new DataOutputStream(fstream);
			BufferedWriter FW = new BufferedWriter(new OutputStreamWriter(out,
					"UTF-8"));
			FW.write(text);
			FW.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
	}

	public static boolean write(byte[] array, File file, boolean append)
	{
		if (file.getParentFile().mkdirs())
		{
			try
			{
				FileOutputStream fos = new FileOutputStream(file, append);
				BufferedOutputStream bos = new BufferedOutputStream(fos, 65536);
				bos.write(array);
				bos.flush();
				bos.close();
				return true;
			}
			catch (IOException e)
			{
				SystemEssentials.Logger.log(Level.WARNING, e);
				return false;
			}
		}
		return false;
	}

	public static int getLineCount(File file)
	{
		int count = 0;
		try
		{
			FileInputStream fstream = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			while (br.readLine() != null)
			{
				count++;
			}
			br.close();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		return count;
	}

	public static String readString(ClassLoader c, String path, String encoding)
	{
		StringBuilder result = new StringBuilder();
		boolean first = true;
		try
		{
			InputStream fstream = c.getResourceAsStream(path);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					encoding));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (!first) strLine = "\n" + strLine;
				else first = false;
				result.append(strLine);
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return result.toString();
	}

	public static String readString(ClassLoader c, String path)
	{
		return readString(c, path, "UTF-8");
	}

	public static String readString(Class c, String path, String encoding)
	{
		return readString(c.getClassLoader(), path.charAt(0) != '/' ? path
				: new String(path.substring(1, path.length())), encoding);
	}

	public static String readString(Class c, String path)
	{
		return readString(c.getClassLoader(), path.charAt(0) != '/' ? path
				: new String(path.substring(1, path.length())), "UTF-8");
	}

	public static String readStringUntilLine(Class c, String path, int line)
	{
		StringBuilder result = new StringBuilder();
		boolean first = true;
		int counter = 1;
		try
		{
			InputStream fstream = c.getResourceAsStream(path);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (counter == line) break;
				else counter++;
				if (!first) strLine = "\n" + strLine;
				else first = false;
				result.append(strLine);
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return result.toString();
	}

	public static String fetchLine(Class c, String path, int lineNo)
	{
		try
		{
			InputStream fstream = c.getResourceAsStream(path);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			int counter = 1;
			while ((strLine = br.readLine()) != null)
			{
				if (counter == lineNo)
				{
					br.close();
					return strLine;
				}
				counter++;
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return null; // not found
	}

	public static String readFirstLine(Class c, String path)
	{
		return fetchLine(c, path, 1);
	}

	public static String readLastLine(Class c, String path)
	{
		String res = null;
		try
		{
			InputStream fstream = c.getResourceAsStream(path);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				res = strLine;
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return res;
	}

	public static String findValue(Class c, String path, String value)
	{
		try
		{
			InputStream fstream = c.getResourceAsStream(path);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (strLine.contains(value))
				{
					br.close();
					return strLine;
				}
				strLine = null;
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return null; // not found

	}

	public static String fetchValue(Class c, String path, String valueStart,
			boolean returnFull)
	{
		try
		{
			InputStream fstream = c.getResourceAsStream(path);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in,
					"UTF-8"));
			String strLine;
			while ((strLine = br.readLine()) != null)
			{
				if (strLine.startsWith(valueStart))
				{
					br.close();
					if (returnFull)
					{
						return strLine;
					}
					else
					// return remaining portion of the string
					{
						int index = strLine.indexOf(valueStart);
						return new String(strLine.substring(index
								+ valueStart.length()));
					}
				}
				strLine = null;
			}
			br.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
		return null; // not found
	}

	public static void copy(InputStream in, OutputStream out)
			throws IOException
	{
		byte[] buffer = new byte[1024];
		int len;
		while ((len = in.read(buffer)) >= 0)
			out.write(buffer, 0, len);
		in.close();
		out.close();
	}

	public static String getRunningClassPath(Class c)
	{
		String res = c.getProtectionDomain().getCodeSource().getLocation()
				.getPath();
		try
		{
			res = URLDecoder.decode(res, "UTF-8");
		}
		catch (UnsupportedEncodingException ex)
		{
		}
		return res;
	}

	public static void removeFolder(File dir)
	{
		String[] val = dir.list();
		if (val != null)
		{
			for (int i = 0; i < val.length; i++)
			{
				File f = dir.getParent() != null ? new File(dir.getParent()
						+ File.separator + dir.getName() + File.separator
						+ val[i]) : new File(dir.getName() + File.separator
						+ val[i]);
				if (f.isFile()) f.delete();
				else removeFolder(f);
			}
		}
		dir.delete();
	}

	public static long getLength(URL u)
	{
		int size = 0;
		HttpURLConnection conn = null;
		try
		{
			conn = (HttpURLConnection) u.openConnection();
			conn.setRequestMethod("HEAD");
			conn.setRequestProperty("User-Agent", "Test");
			conn.setRequestProperty("Connection", "close");
			conn.getInputStream();
			size = conn.getContentLength();
			return size;
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
			return -1;
		}
		finally
		{
			conn.disconnect();
		}
	}

	public static long getLastModified(URL u)
	{
		long size = 0;
		URLConnection conn;
		try
		{
			conn = u.openConnection();
			size = conn.getLastModified();
			return size;
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
			return -1;
		}
	}

	public static boolean gotInternetConnection(int timeout)
	{
		URL u = null;
		try
		{
			u = new URL("http://www.google.com");
		}
		catch (MalformedURLException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
			return false;
		}
		return (IOEssentials.exists(u, timeout));
	}

	public static boolean gotInternetConnection()
	{
		return gotInternetConnection(NETWORK_TIMEOUT_SHORT);
	}

	public static boolean exists(URL url, int timeout)
	{
		try
		{
			HttpURLConnection urlc = (HttpURLConnection) (url.openConnection());
			urlc.setRequestProperty("User-Agent", "Test");
			urlc.setRequestProperty("Connection", "close");
			urlc.setConnectTimeout(timeout);
			urlc.connect();
			return (urlc.getResponseCode() == 200);
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
			return false;
		}
	}

	public static boolean download(File dest, URL source, boolean directTransfer)
	{
		if (dest.getParentFile() != null) dest.getParentFile().mkdirs();
		if (dest.exists() && dest.isFile()) dest.delete();
		if (directTransfer)
		{
			FileOutputStream fos = null;
			ReadableByteChannel rbc = null;
			try
			{
				long len = getLength(source);
				if (len != -1)
				{
					rbc = Channels.newChannel(source.openStream());
					fos = new FileOutputStream(dest);
					fos.getChannel().transferFrom(rbc, 0,
							len <= 0 ? Integer.MAX_VALUE : len);
				}
				else return false;
			}
			catch (IOException e)
			{
				SystemEssentials.Logger.log(Level.WARNING, e);
				return false;
			}
			finally
			{
				closeQuietly(rbc);
				closeQuietly(fos);
			}
			return true;
		}

		else
		// Slower but more compatible (Android)
		{
			InputStream input = null;
			OutputStream output = null;
			URLConnection connection = null;
			try
			{
				connection = source.openConnection();
				connection.connect();

				input = new BufferedInputStream(connection.getInputStream());
				output = new FileOutputStream(dest);

				byte data[] = new byte[8192];
				int count;
				long writeCount = 0;
				while ((count = input.read(data)) != -1)
				{
					writeCount += count;
					output.write(data, 0, count);
				}
				if (writeCount <= 0) return false;
			}
			catch (Exception e)
			{
				SystemEssentials.Logger.log(Level.WARNING, e);
				return false;
			}
			finally
			{
				closeQuietly(output);
				closeQuietly(input);
			}
			return true;
		}
	}

	public static boolean download(File dest, URL source)
	{
		return download(dest, source, false);
	}

	public static void closeQuietly(Closeable closeable)
	{
		if (closeable == null) return;
		try
		{
			if (closeable instanceof OutputStream)
			{
				((OutputStream) closeable).flush();
			}
			closeable.close();
		}
		catch (IOException e)
		{
			SystemEssentials.Logger.log(Level.WARNING, e);
		}
	}

	public static File getRandomTempFile()
	{
		return getRandomTempFile(null);
	}

	public static File getRandomTempFile(String parent)
	{
		File f = new File(parent, MathEssentials.newRandom() + ".temp");
		if (f.exists()) return getRandomTempFile();
		else return f;
	}

	public static void deleteTempFiles(File dir)
	{
		String[] list = dir.list();
		for (int i = 0; i < list.length; i++)
		{
			if (list[i].endsWith(".temp"))
				new File(dir.getPath(), list[i]).delete();
		}
	}

	public static boolean update(File file, URL url)
	{
		return update(file, url, FILE_UPDATE_POLICY_IF_POSSIBLE);
	}

	public static boolean update(File file, URL url, int updatePolicy)
	{
		SystemEssentials.Logger.log(Level.INFO, "Updating " + file + "...");

		if (updatePolicy == FILE_UPDATE_POLICY_NEVER && file.exists())
			return true;
		if (!gotInternetConnection())
		{
			if (!file.exists() || updatePolicy == FILE_UPDATE_POLICY_FORCED)
			{
				return false;
			}
			else return true;
		}

		switch (updatePolicy)
		{
		case (FILE_UPDATE_POLICY_NEVER):
			if (!file.exists())
			{
				if (url == null) return false;
				else
				{
					return download(file, url);
				}
			}
			else return true;

		case (FILE_UPDATE_POLICY_IF_POSSIBLE):
			if (!file.exists())
			{
				if (url == null) return false;
				else
				{
					return download(file, url);
				}
			}
			else
			{
				if (url == null) return true;
				else
				{
					long size = getLength(url);

					if (size != file.length())
					{
						if (file.delete())
						{
							return download(file, url);
						}

						else
						{
							File temp = getRandomTempFile(file.getParent());
							if (download(temp, url))
							{
								boolean v = copy(temp, file);
								temp.deleteOnExit();
								if (v && file.exists()) return true;
								else return false;
							}
							else
							{
								temp.deleteOnExit();
								return true;
							}
						}
					}
					else return true;
				}
			}

		case (FILE_UPDATE_POLICY_STRICT): // obtain the file and if
											// it can be
			// replaced, enforce update
			if (!file.exists())
			{
				if (url == null) return false;
				else
				{
					return download(file, url);
				}
			}
			else
			{
				if (url == null) return true;
				else
				{
					long size = getLength(url);

					if (size != -1 && size != file.length())
					{
						File temp = getRandomTempFile(file.getParent());
						boolean val = download(temp, url);
						if (val)
						{
							boolean v = copy(temp, file);
							temp.delete();
							return v;
						}
						temp.delete();
						return val;
					}
					else return true;
				}
			}

		case (FILE_UPDATE_POLICY_FORCED):
			if (!file.exists())
			{
				if (url == null) return false;
				else
				{
					return download(file, url);
				}
			}
			else
			{
				if (url == null) return false;
				else
				{
					long size = getLength(url);

					if (size != -1 && size != file.length())
					{
						File temp = getRandomTempFile(file.getParent());
						boolean val = download(temp, url);
						if (val)
						{
							boolean v = copy(temp, file);
							temp.delete();
							return v;
						}
						temp.delete();
						return val;
					}
					else if (size == -1) return false;
					else return true;
				}
			}

		default:
			return update(file, url, FILE_UPDATE_POLICY_IF_POSSIBLE);

		}
	}

	public static URL toURL(String path)
	{
		URL res = null;
		try
		{
			res = new URL(path);
		}
		catch (MalformedURLException ex)
		{
		}
		return res;
	}

}
